package bancosys.tec.utils.factory;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.FactoryBean;

/**
 * Cria uma lista de classes. Forneça no construtor a superclasse dos elementos que devem ser aceitos na construção da lista de classes.
 * <p>
 * Todos os métodos da interface FactoryBean menos getObject() não são final e podem ser sobreescritos por especializações.
 * 
 * @author jrenaut
 */
public abstract class ClassListFactoryBean implements FactoryBean {

    private Log log = LogFactory.getLog(ClassListFactoryBean.class);

    private Set<Class<?>> classes = new HashSet<Class<?>>();

    private String name = "";

    /**
     * Retorna a lista de classes.
     * 
     * @return a lista de classes.
     */
    public final Object getObject() {
        for (Class<?> clazz : this.classes) {
            this.log.debug(this.name + " > " + clazz.getName());
        }
        return this.doGetList(this.classes);
    }

    /**
     * Hook para especializações determinarem como construir a lista de classes que será devolvida.
     * <p>
     * A implementação default retorna um LinkedList com o conjunto de classes construido.
     * 
     * @param allClasses O conjunto de classes
     * @return a lista com as classes
     */
    protected List<Class<?>> doGetList(Set<Class<?>> allClasses) {
        return new LinkedList<Class<?>>(allClasses);
    }

    /**
     * Decide se determinada classe deve ou não ser adicionada ao conjunto.
     * 
     * @param clazz a classe sendo testada
     * @return true se deve adicionar
     */
    protected final boolean accept(Class<?> clazz) {
        if (clazz != null) {
            return this.doAccept(clazz);
        }
        return false;
    }

    /**
     * Delega para a implementação.
     * 
     * @param clazz a classe que deve ser testada (nunca será null)
     * @return boolean
     */
    protected abstract boolean doAccept(Class<?> clazz);

    /**
     * Define as classes mapeadas por essa factory
     * 
     * @param mappingResources a lista de classes persistíveis.
     */
    public final void setMappingResources(List<Class<?>> mappingResources) {
        if (mappingResources != null) {
            Iterator<Class<?>> it = mappingResources.iterator();
            while (it.hasNext()) {
                Class<?> clazz = it.next();
                if (this.accept(clazz)) {
                    if (this.classes.contains(clazz)) {
                        this.log.debug(this.name + " = " + clazz.getName());
                    } else {
                        this.log.debug(this.name + " < " + clazz.getName());
                        this.classes.add(clazz);
                    }
                } else {
                    String cname = clazz != null ? clazz.getName() : "null";
                    this.log.debug(this.name + " ! " + cname);
                }
            }
        } else {
            this.log.warn(this.name + ": mappingResources == null");
        }
    }

    /**
     * Define as dependências dessa factory
     * 
     * @param dependencies lista das listas de dependência.
     */
    public final void setDependencies(List<List<Class<?>>> dependencies) {
        if (dependencies != null) {
            Iterator<List<Class<?>>> it = dependencies.iterator();
            while (it.hasNext()) {
                List<Class<?>> dependency = it.next();
                this.setMappingResources(dependency);
            }
        } else {
            this.log.warn(this.name + ": dependencies == null");
        }
    }

    /**
     * Define o nome dessa factory.
     * 
     * @param name o nome da factory.
     */
    public final void setName(String name) {
        this.name = name;
    }

    /**
     * @return the name
     */
    public final String getName() {
        return this.name;
    }

    /**
     * Devovle o tipo de objetos que essa factory cria.
     * 
     * @return o tipo de objetos que essa factory cria.
     */
    public Class<?> getObjectType() {
        return List.class;
    }

    /**
     * O objeto gerenciado por esssa factory é singleton?
     * 
     * @return <code>true</code> caso afirmativo, <code>false</code> caso contrário.
     */
    public boolean isSingleton() {
        return false;
    }

}
